/*
 * MessageSequence.java
 *
 * Created on April 18, 2007, 8:32 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.xeerkat.restlet.xmpp;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.UUID;
import java.util.logging.Logger;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.DocumentSource;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Cookie;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Parameter;
import org.restlet.representation.Representation;
import org.xeerkat.restlet.xmpp.util.Base64Encoder;

/**
 *
 * @author alex
 */
public abstract class MessageSequence { 

   static Logger LOG = Logger.getLogger(MessageSequence.class.getName());
   
   // 32K chunks gives us approximately 64K base64
   public static long MAX_SIZE = 32*1024*1024;
   
   public static URI NAMESPACE = URI.create("http://www.xeerkat.org/Vocabulary/Resource/2007/1/0");
   public static final Name RESPONSE = Name.create(NAMESPACE,"response");
   public static final Name REQUEST = Name.create(NAMESPACE,"request");
   public static final Name AUTHORIZATION = Name.create(NAMESPACE,"authorization");
   public static final Name COOKIE = Name.create(NAMESPACE,"cookie");
   public static final Name CHUNK = Name.create(NAMESPACE,"chunk");
   public static final Name END = Name.create(NAMESPACE,"end");
   public static final Name HEADER = Name.create(NAMESPACE,"header");
   public static final Name BODY = Name.create(NAMESPACE,"body");
   public static URI AGENT_NAMESPACE = URI.create("http://www.xeerkat.org/Vocabulary/Agent/2008/1/0");
   public static final Name MANAGE = Name.create(AGENT_NAMESPACE,"manage");
   
   UUID id;
   Request request;
   Response response;
   Document document;
   String path;
   ItemConstructor constructor;
   /** Creates a new instance of MessageSequence */
   public MessageSequence(Request request,String path)
   {
      this.constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      this.id = UUID.randomUUID();
      this.request = request;
      this.response = null;
      this.document = null;
      this.path = path;
   }
   
   public MessageSequence(UUID id,Response response)
   {
      this.constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      this.id = id;
      this.request = null;
      this.response = response;
      this.document = null;
      this.path = response.getRequest().getResourceRef().getPath();
   }
   
   public MessageSequence(UUID id,Document document)
   {
      this.constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
      this.id = null;
      this.request = null;
      this.response = null;
      this.document = document;
      this.path = null;
   }
   
   public UUID getId() {
      return id;
   }
   
   public abstract ItemDestination onMessage()
      throws XMPPException;

   public void generate() 
      throws IOException,XMLException,XMPPException
   {
      if (response!=null) {
         generateResponse();
      } else if (request!=null) {
         generateRequest();
      } else {
         DocumentSource.generate(document, false, onMessage());
      }
   }
   
   protected void generateResponse()
      throws IOException,XMLException,XMPPException
   {
      Method method = response.getRequest().getMethod();
      Representation rep = response.getEntity();
      if (rep==null) {
         ItemDestination dest = onMessage();
         dest.send(constructor.createDocument());
         Element responseE = constructor.createElement(RESPONSE);
         responseE.setAttributeValue("status",Integer.toString(response.getStatus().getCode()));
         responseE.setAttributeValue("id",id.toString());
         responseE.setAttributeValue("method",method.toString());
         responseE.setAttributeValue("path",path);
         dest.send(responseE);
         getHeaders(dest);
         dest.send(constructor.createElementEnd(RESPONSE));
         dest.send(constructor.createDocumentEnd());
      } else {
         long size = rep.getSize();
         MediaType type = rep.getMediaType();
         if (type==null) {
            type = MediaType.APPLICATION_OCTET_STREAM;
         }
         String mainType = type.getMainType();
         if (mainType.equals("text") || type.getName().endsWith("xml")) {
            // we're sending unicode data
            // TODO: This might not be so good for large text...
            // TODO: optimize escaping
            try {
               ItemDestination dest = onMessage();
               dest.send(constructor.createDocument());
               Element responseE = constructor.createElement(RESPONSE);
               responseE.setAttributeValue("status",Integer.toString(response.getStatus().getCode()));
               responseE.setAttributeValue("id",id.toString());
               responseE.setAttributeValue("method",method.toString());
               responseE.setAttributeValue("path",path);
               dest.send(responseE);
               getHeaders(dest);
               Element bodyE = constructor.createElement(BODY);
               bodyE.setAttributeValue("type",type.getName());
               dest.send(bodyE);
               dest.send(constructor.createCharacters(rep.getText()));
               dest.send(constructor.createElementEnd(BODY));
               dest.send(constructor.createElementEnd(RESPONSE));
               dest.send(constructor.createDocumentEnd());
            } catch (Exception ex) {
               ex.printStackTrace();
               throw new IOException("Failed to send message due to exception: "+ex.getMessage());
            }
         } else {
            ItemDestination dest = onMessage();
            dest.send(constructor.createDocument());
            Element responseE = constructor.createElement(RESPONSE);
            responseE.setAttributeValue("status",Integer.toString(response.getStatus().getCode()));
            responseE.setAttributeValue("id",id.toString());
            responseE.setAttributeValue("method",method.toString());
            responseE.setAttributeValue("path",path);
            dest.send(responseE);
            getHeaders(dest);
            Element bodyE = constructor.createElement(BODY);
            bodyE.setAttributeValue("type",type.getName());
            bodyE.setAttributeValue("chucked","true");
            if (size>0) {
               bodyE.setAttributeValue("size",Long.toString(size));
            }
            dest.send(bodyE);
            dest.send(constructor.createElementEnd(BODY));
            dest.send(constructor.createElementEnd(RESPONSE));
            dest.send(constructor.createDocumentEnd());
            chunk(rep);
         }

      }
      
   }
   protected void generateRequest() 
      throws IOException,XMLException,XMPPException
   {
      Method method = request.getMethod();
      if (method.equals(Method.GET) || method.equals(Method.HEAD) || method.equals(Method.DELETE)) {
         ItemDestination dest = onMessage();
         dest.send(constructor.createDocument());
         Element requestE = constructor.createElement(REQUEST);
         requestE.setAttributeValue("id",id.toString());
         requestE.setAttributeValue("method",method.toString());
         requestE.setAttributeValue("path",path);
         dest.send(requestE);
         if (request.getChallengeResponse()!=null) {
            Element authE = constructor.createElement(AUTHORIZATION);
            authE.setAttributeValue("scheme",request.getChallengeResponse().getScheme().toString());
            authE.setAttributeValue("id",request.getChallengeResponse().getIdentifier());
            authE.setAttributeValue("secret",new String(request.getChallengeResponse().getSecret()));
            dest.send(authE);
            dest.send(constructor.createElementEnd(AUTHORIZATION));
         }
         for (Cookie cookie : request.getCookies()) {
            //LOG.info("Adding cookie "+cookie.getName()+" to request.");
            Element cookieE = constructor.createElement(COOKIE);
            cookieE.setAttributeValue("name",cookie.getName());
            cookieE.setAttributeValue("value",cookie.getValue());
            if (cookie.getDomain()!=null) {
               cookieE.setAttributeValue("domain",cookie.getDomain());
            }
            if (cookie.getPath()!=null) {
               cookieE.setAttributeValue("path",cookie.getPath());
            }
            dest.send(cookieE);
            dest.send(constructor.createElementEnd(COOKIE));
         }
         getHeaders(dest);
         dest.send(constructor.createElementEnd(REQUEST));
         dest.send(constructor.createDocumentEnd());
         
      } else if (method.equals(Method.POST) || method.equals(Method.PUT)) {
         ItemDestination dest = onMessage();
         Representation rep = request.getEntity();
         if (rep==null) {
            dest.send(constructor.createDocument());
            Element requestE = constructor.createElement(REQUEST);
            requestE.setAttributeValue("id",id.toString());
            requestE.setAttributeValue("method",method.toString());
            requestE.setAttributeValue("path",path);
            dest.send(requestE);
            if (request.getChallengeResponse()!=null) {
               Element authE = constructor.createElement(AUTHORIZATION);
               authE.setAttributeValue("scheme",request.getChallengeResponse().getScheme().toString());
               authE.setAttributeValue("id",request.getChallengeResponse().getIdentifier());
               authE.setAttributeValue("secret",new String(request.getChallengeResponse().getSecret()));
               dest.send(authE);
               dest.send(constructor.createElementEnd(AUTHORIZATION));
            }
            for (Cookie cookie : request.getCookies()) {
              // LOG.info("Adding cookie "+cookie.getName()+" to request.");
               Element cookieE = constructor.createElement(COOKIE);
               cookieE.setAttributeValue("name",cookie.getName());
               cookieE.setAttributeValue("value",cookie.getValue());
               if (cookie.getDomain()!=null) {
                  cookieE.setAttributeValue("domain",cookie.getDomain());
               }
               if (cookie.getPath()!=null) {
                  cookieE.setAttributeValue("path",cookie.getPath());
               }
               dest.send(cookieE);
               dest.send(constructor.createElementEnd(COOKIE));
            }
            getHeaders(dest);
            dest.send(constructor.createElementEnd(REQUEST));
            dest.send(constructor.createDocumentEnd());
            return;
         }
         long size = rep.getSize();
         //System.out.println("Sending media type: "+rep.getMediaType().getName());
         if (rep.getMediaType().getMainType().equals("text") || rep.getMediaType().getName().endsWith("xml")) {
            // we're sending unicode data
            // TODO: This might not be so good for large text...
            // TODO: optimize escaping
            dest.send(constructor.createDocument());
            Element requestE = constructor.createElement(REQUEST);
            requestE.setAttributeValue("id",id.toString());
            requestE.setAttributeValue("method",method.toString());
            requestE.setAttributeValue("path",path);
            dest.send(requestE);
            if (request.getChallengeResponse()!=null) {
               Element authE = constructor.createElement(AUTHORIZATION);
               authE.setAttributeValue("scheme",request.getChallengeResponse().getScheme().toString());
               authE.setAttributeValue("id",request.getChallengeResponse().getIdentifier());
               authE.setAttributeValue("secret",new String(request.getChallengeResponse().getSecret()));
               dest.send(authE);
               dest.send(constructor.createElementEnd(AUTHORIZATION));
            }
            for (Cookie cookie : request.getCookies()) {
              // LOG.info("Adding cookie "+cookie.getName()+" to request.");
               Element cookieE = constructor.createElement(COOKIE);
               cookieE.setAttributeValue("name",cookie.getName());
               cookieE.setAttributeValue("value",cookie.getValue());
               if (cookie.getDomain()!=null) {
                  cookieE.setAttributeValue("domain",cookie.getDomain());
               }
               if (cookie.getPath()!=null) {
                  cookieE.setAttributeValue("path",cookie.getPath());
               }
               dest.send(cookieE);
               dest.send(constructor.createElementEnd(COOKIE));
            }
            getHeaders(dest);
            Element bodyE = constructor.createElement(BODY);
            bodyE.setAttributeValue("type",rep.getMediaType().getName());
            dest.send(bodyE);
            dest.send(constructor.createCharacters(rep.getText()));
            dest.send(constructor.createElementEnd(BODY));
            dest.send(constructor.createElementEnd(REQUEST));
            dest.send(constructor.createDocumentEnd());
         } else {
            dest.send(constructor.createDocument());
            Element requestE = constructor.createElement(REQUEST);
            requestE.setAttributeValue("id",id.toString());
            requestE.setAttributeValue("method",method.toString());
            requestE.setAttributeValue("path",path);
            dest.send(requestE);
            if (request.getChallengeResponse()!=null) {
               Element authE = constructor.createElement(AUTHORIZATION);
               authE.setAttributeValue("scheme",request.getChallengeResponse().getScheme().toString());
               authE.setAttributeValue("id",request.getChallengeResponse().getIdentifier());
               authE.setAttributeValue("secret",new String(request.getChallengeResponse().getSecret()));
               dest.send(authE);
               dest.send(constructor.createElementEnd(AUTHORIZATION));
            }
            for (Cookie cookie : request.getCookies()) {
              // LOG.info("Adding cookie "+cookie.getName()+" to request.");
               Element cookieE = constructor.createElement(COOKIE);
               cookieE.setAttributeValue("name",cookie.getName());
               cookieE.setAttributeValue("value",cookie.getValue());
               if (cookie.getDomain()!=null) {
                  cookieE.setAttributeValue("domain",cookie.getDomain());
               }
               if (cookie.getPath()!=null) {
                  cookieE.setAttributeValue("path",cookie.getPath());
               }
               dest.send(cookieE);
               dest.send(constructor.createElementEnd(COOKIE));
            }
            getHeaders(dest);
            Element bodyE = constructor.createElement(BODY);
            bodyE.setAttributeValue("type",rep.getMediaType().getName());
            bodyE.setAttributeValue("chucked","true");
            if (size>0) {
               bodyE.setAttributeValue("size",Long.toString(size));
            }
            dest.send(bodyE);
            dest.send(constructor.createElementEnd(BODY));
            dest.send(constructor.createElementEnd(REQUEST));
            dest.send(constructor.createDocumentEnd());
            chunk(rep);
         }
      }
   }
   
   protected void chunk(Representation rep) 
      throws IOException,XMLException,XMPPException
   {
      if (rep==null) {
         return;
      }
      OutputStream os = new OutputStream() {
         byte [] buffer = new byte[(int)MAX_SIZE];
         int pos = 0;
         public void write(int b) 
            throws IOException
         {
            buffer[pos++] = (byte)b;
            if (pos==buffer.length) {
               sendChunk();
               pos = 0;
            }
         }
         public void write(byte [] b) 
            throws IOException
         {
            write(b,0,b.length);
         }
         public void write(byte [] b,int offset,int len) 
            throws IOException
         {
            int remaining = pos + len - buffer.length;
            if (remaining>=0) {
               // copy the first part
               System.arraycopy(b,offset,buffer,pos,len-remaining);

               // send the chunk
               sendChunk();

               // copy the remaining
               System.arraycopy(b,offset+remaining,buffer,0,remaining);
               pos = remaining;
            } else {
               // copy the whole thing
               System.arraycopy(b,offset,buffer,pos,len);
               pos += len;
            }
         }
         void sendChunk() 
            throws IOException
         {
            Base64Encoder encoder = new Base64Encoder();
            encoder.translate(buffer,0,pos);
            String value = encoder.toString();
            try {
               ItemDestination dest = onMessage();
               dest.send(constructor.createDocument());
               Element chunkE = constructor.createElement(CHUNK);
               chunkE.setAttributeValue("id",id.toString());
               dest.send(chunkE);
               dest.send(constructor.createCharacters(value));
               dest.send(constructor.createElementEnd(CHUNK));
               dest.send(constructor.createDocumentEnd());
            } catch (XMLException ex) {
               throw new IOException(ex.getMessage());
            } catch (XMPPException ex) {
               throw new IOException(ex.getMessage());
            }
         }
         public void flush() 
            throws IOException
         {
            if (pos>0) {
               sendChunk();
            }
         }
      };
      rep.write(os);
      os.flush();
      ItemDestination dest = onMessage();
      dest.send(constructor.createDocument());
      Element endE = constructor.createElement(END);
      endE.setAttributeValue("id",id.toString());
      dest.send(endE);
      dest.send(constructor.createElementEnd(END));
      dest.send(constructor.createDocumentEnd());
      
   }
   
   private void getHeaders(ItemDestination dest) 
      throws XMLException
   {
      Form headers = request==null ? (Form)response.getAttributes().get("org.restlet.http.headers") : (Form)request.getAttributes().get("org.restlet.http.headers");
      if (headers==null) {
         return;
      }
      for (Parameter param : headers) {
         Element headerE = constructor.createElement(HEADER);
         headerE.setAttributeValue("name",param.getName());
         headerE.setAttributeValue("value",param.getValue());
         dest.send(headerE);
         dest.send(constructor.createElementEnd(HEADER));
      }
   }

   /*
   private String escape(String input,boolean inAttribute) {
      StringBuilder builder = new StringBuilder();
      int i = 0; int start = 0; int len = input.length();
      for (; i<len; i++) {
         switch (input.charAt(i)) {
            case '<':
               builder.append(input.substring(start,i));
               builder.append("&lt;");
               start = i+1;
               break;
            case '&':
               builder.append(input.substring(start,i));
               builder.append("&amp;");
               start = i+1;
               break;
            case '\'':
               if (inAttribute) {
                  builder.append(input.substring(start,i));
                  builder.append("&apos;");
                  start = i+1;
               }
               break;
         }
      }
      if (start<len) {
         builder.append(input.substring(start));
      }
      return builder.toString();
   }
    */
   
   public void remove() {
      throw new UnsupportedOperationException("Cannot remove elements from message.");
   }
   
}
